//!
//! Client-end operations
//!

use crate::RT;
use dzk::{
    cfg::{CliBalanceArgs, CliCfg, CliOps, CliTransferArgs},
    common::to_hex,
};
use eth_utils::ecdsa_keys::SecpPair;
use ethereum_types::H160;
use libsecp256k1::SecretKey;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use rand::random;
use ruc::*;
use secp256k1::SecretKey as Web3SecKey;
use std::{fs, mem, str::FromStr};
use tokio::time::{sleep, Duration, Instant};
use web3::{
    contract::{Contract, Options},
    signing::SecretKeyRef,
    transports::Http,
    types::{Address, TransactionParameters, H256, U256},
    Web3,
};

static LK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

pub async fn exec(cfg: CliCfg) -> Result<()> {
    match cfg.commands {
        CliOps::Gen(cfg) => {
            let mut hdrs = vec![];
            for _ in 0..cfg.cnt {
                hdrs.push(RT.spawn(async {
                    let (addr, phrase) = create_account();
                    let mut lk = LK.lock();
                    println!("\x1b[31;1mAddress:\x1b[0m 0x{:x}", addr);
                    println!("\x1b[31;1mPhrase:\x1b[0m {}", phrase);
                    *lk = ();
                }));
            }
            for hdr in hdrs.into_iter() {
                hdr.await.c(d!())?;
            }
            Ok(())
        }
        CliOps::Addrof(cfg) => {
            let phrase = if let Some(file) = cfg.phrase_file.as_ref() {
                fs::read_to_string(file).c(d!())?
            } else if let Some(phrase) = cfg.phrase.as_ref() {
                phrase.to_owned()
            } else {
                return Err(eg!("Neither 'phrase file' nor 'phrase' provided!"));
            };
            recover_account_from_phrase(&phrase)
                .c(d!())
                .map(|(addr, _)| println!("0x{:x}", addr))
        }
        CliOps::Balance(cfg) => balance(&cfg).await.c(d!()),
        CliOps::Transfer(cfg) => transfer(&cfg).await.c(d!()),
    }
}

macro_rules! parse_serv {
    ($i: expr) => {{
        if $i.use_mainnet {
            vec!["https://prod-mainnet.prod.microverus.io:8545".to_owned()]
        } else if $i.use_testnet {
            vec!["https://prod-testnet.prod.microverus.io:8545".to_owned()]
        } else if $i.use_qa01net {
            vec!["https://dev-qa01.dev.microverus.io:8545".to_owned()]
        } else if $i.use_qa02net {
            vec!["https://dev-qa02.dev.microverus.io:8545".to_owned()]
        } else {
            if $i.use_custom_net.is_empty() {
                return Err(eg!("Server-end RPC endpoint not found"));
            }
            $i.use_custom_net
                .split(',')
                .map(|i| i.to_owned())
                .collect::<Vec<_>>()
        }
    }};
}

macro_rules! print_receipt {
    ($receipt: expr) => {{
        let msg = pnk!(serde_json::to_string_pretty(&$receipt));
        let mut lk = LK.lock();
        println!("{}", msg);
        *lk = ();
    }};
}

#[inline(always)]
async fn balance(cfg: &CliBalanceArgs) -> Result<()> {
    let hdr = Http::new(&parse_serv!(cfg)[0]).c(d!()).map(Web3::new)?;
    let addr = Address::from_str(&cfg.addr).c(d!())?;
    let contract = cfg.contract_addr.as_deref();
    balance_readable(&hdr, addr, contract)
        .await
        .c(d!())
        .map(|am| {
            println!("{}", am);
        })
}

#[inline(always)]
async fn balance_readable(
    hdr: &Web3<Http>,
    addr: Address,
    contract_addr: Option<&str>,
) -> Result<String> {
    balance_of(hdr, addr, contract_addr)
        .await
        .c(d!())
        .map(to_float_str)
}

#[inline(always)]
async fn balance_of(
    hdr: &Web3<Http>,
    addr: Address,
    contract_addr: Option<&str>,
) -> Result<u128> {
    if let Some(contract) = contract_addr {
        balance_of_contract_token(hdr, addr, contract).await.c(d!())
    } else {
        balance_of_native_token(hdr, addr).await.c(d!())
    }
}

#[inline(always)]
async fn balance_of_contract_token(
    hdr: &Web3<Http>,
    addr: Address,
    contract_addr: &str,
) -> Result<u128> {
    let contract = Contract::from_json(
        hdr.eth(),
        Address::from_str(contract_addr).c(d!(contract_addr))?,
        include_bytes!("erc20.abi"),
    )
    .c(d!())?;
    let balance: U256 = contract
        .query("balanceOf", addr, None, Options::default(), None)
        .await
        .c(d!())?;
    Ok(balance.as_u128())
}

#[inline(always)]
async fn balance_of_native_token(hdr: &Web3<Http>, addr: Address) -> Result<u128> {
    hdr.eth()
        .balance(addr, None)
        .await
        .c(d!())
        .map(|am| am.as_u128())
}

async fn transfer(cfg: &CliTransferArgs) -> Result<()> {
    let hdrs = parse_serv!(cfg)
        .into_iter()
        .map(|s| Http::new(&s).c(d!(s)).map(Web3::new))
        .collect::<Result<Vec<_>>>()?;

    if cfg.batch_mode {
        return transfer_batch(hdrs.as_slice(), cfg).await.c(d!());
    }

    // Solo usage in one-to-one mode.
    let hdr = &hdrs[0];

    let phrase = if let Some(file) = cfg.cfg_file.as_ref() {
        fs::read_to_string(file).c(d!())?
    } else if let Some(phrase) = cfg.phrase.as_ref() {
        phrase.to_owned()
    } else {
        return Err(eg!("Neither '--cfg-file' nor '--phrase' found!"));
    };
    let prvk = recover_prvk_from_phrase(&phrase).c(d!())?;
    let to = cfg
        .receiver
        .as_deref()
        .c(d!("Receiver should not be empty!"))
        .and_then(|r| Address::from_str(r).c(d!()))?;
    let amount = cfg
        .amount
        .as_deref()
        .c(d!("Amount should not be empty!"))
        .and_then(|am_str| am_str_to_u128(am_str).c(d!()))?;

    let txhash = if let Some(contract) = cfg.contract_addr.as_deref() {
        transfer_contract_token(hdr, &prvk, to, amount, contract)
            .await
            .c(d!())?
    } else {
        transfer_native_token(hdr, &prvk, to, amount)
            .await
            .c(d!())?
    };

    if cfg.wait_receipt {
        sleep(Duration::from_secs(5)).await;
        hdr.eth()
            .transaction_receipt(txhash)
            .await
            .c(d!())
            .map(|receipt| print_receipt!(receipt))
    } else {
        Ok(())
    }
}

async fn transfer_batch(hdrs: &[Web3<Http>], cfg: &CliTransferArgs) -> Result<()> {
    let entries = if let Some(file) = cfg.cfg_file.as_ref() {
        fs::read(file)
            .c(d!())
            .and_then(|c| serde_json::from_slice::<EntriesRaw>(&c).c(d!()))
            .and_then(|entries_raw| Entries::try_from(entries_raw).c(d!()))?
    } else {
        return Err(eg!("'--cfg-file' missing"));
    };

    let total = entries.0.len();
    println!("=== Total entries: {} ===", total);

    let mut txhashes = vec![];
    let mut errors = vec![];
    if let Some(contract) = cfg.contract_addr.as_ref() {
        for (idx, e) in entries.0.iter().enumerate() {
            let _ = transfer_contract_token(
                &hdrs[idx % hdrs.len()],
                &e.prvk,
                e.to,
                e.am,
                contract,
            )
            .await
            .c(d!())
            .map(|txhash| {
                if txhash.is_zero() {
                    errors.push("tx hash is null".to_string());
                } else {
                    txhashes.push((txhash, 0));
                }
            })
            .map_err(|e| {
                errors.push(e.to_string());
            });
        }
    } else {
        for (idx, e) in entries.0.iter().enumerate() {
            let _ = transfer_native_token(&hdrs[idx % hdrs.len()], &e.prvk, e.to, e.am)
                .await
                .c(d!())
                .map(|txhash| {
                    if txhash.is_zero() {
                        errors.push("tx hash is null".to_string());
                    } else {
                        txhashes.push((txhash, 0));
                    }
                })
                .map_err(|e| {
                    errors.push(e.to_string());
                });
        }
    }

    if !cfg.wait_receipt {
        if errors.is_empty() {
            return Ok(());
        } else {
            pd!(pnk!(serde_json::to_string_pretty(&errors)));
            return Err(eg!());
        }
    }

    let start_time = Instant::now();

    let mut fail_in_sending_cnt = errors.len();
    let mut fail_in_execution_cnt = 0;
    let mut success_cnt = 0;
    let mut receipts = vec![];
    let mut cnter = total;
    while fail_in_sending_cnt < cnter {
        sleep(Duration::from_secs(3)).await;
        let hashes = mem::take(&mut txhashes);
        for (h, retry_cnt) in hashes.into_iter() {
            match hdrs[random::<usize>() % hdrs.len()]
                .eth()
                .transaction_receipt(h)
                .await
                .c(d!())
            {
                Ok(Some(receipt)) => {
                    if let Some(status) = receipt.status {
                        if 1 == status.as_u32() {
                            success_cnt += 1;
                        } else {
                            fail_in_execution_cnt += 1;
                        }
                    } else {
                        fail_in_execution_cnt += 1;
                    }
                    receipts.push(receipt);
                    cnter = cnter.saturating_sub(1);
                }
                Ok(None) => {
                    if 9 < retry_cnt {
                        cnter -= 1;
                        errors.push(format!("{}: no receipt", to_hex(h)));
                    } else {
                        txhashes.push((h, 1 + retry_cnt));
                    }
                }
                Err(e) => {
                    cnter -= 1;
                    errors.push(e.to_string());
                }
            }
        }
        fail_in_sending_cnt = errors.len();
    }

    let time_spent = start_time.elapsed().as_secs_f32();
    println!(
        "\x1b[31;01mTPS estimation:\x1b[00m {}",
        total as f32 / time_spent
    );
    println!(
        "\x1b[31;01mTotal time spent(in seconds):\x1b[00m {}",
        time_spent
    );

    println!(
        "\x1b[31;01mNumber of successful txs:\x1b[00m {}",
        success_cnt
    );
    println!(
        "\x1b[31;01mNumber of txs sent failed:\x1b[00m {}",
        fail_in_sending_cnt
    );
    println!(
        "\x1b[31;01mNumber of txs sent successfully but failed to execute:\x1b[00m {}",
        fail_in_execution_cnt
    );

    let error_log = format!("/tmp/dzk_transfer_batch.errors.{}", random::<u32>());
    let receipt_log = format!("/tmp/dzk_transfer_batch.receipts.{}", random::<u32>());

    let errors = pnk!(serde_json::to_vec_pretty(&errors));
    fs::write(&error_log, errors).c(d!())?;
    println!(
        "\x1b[01mError logs have been written to:\x1b[00m {}",
        error_log
    );

    let receipts = pnk!(serde_json::to_vec_pretty(&receipts));
    fs::write(&receipt_log, receipts).c(d!())?;
    println!(
        "\x1b[01mReceipt logs have been written to:\x1b[00m {}",
        receipt_log
    );

    Ok(())
}

async fn transfer_contract_token(
    hdr: &Web3<Http>,
    prvk: &Web3SecKey,
    to: Address,
    am: u128,
    contract_addr: &str,
) -> Result<TxHash> {
    let contract = Contract::from_json(
        hdr.eth(),
        Address::from_str(contract_addr).c(d!())?,
        include_bytes!("erc20.abi"),
    )
    .c(d!())?;

    contract
        .signed_call(
            "transfer",
            (to, am),
            Options::default(),
            SecretKeyRef::new(prvk),
        )
        .await
        .c(d!())
}

async fn transfer_native_token(
    hdr: &Web3<Http>,
    prvk: &Web3SecKey,
    to: Address,
    amount: u128,
) -> Result<TxHash> {
    let tx_object = TransactionParameters {
        to: Some(to),
        value: U256::from(amount),
        ..Default::default()
    };
    let signed = hdr
        .accounts()
        .sign_transaction(tx_object, prvk)
        .await
        .c(d!())?;

    hdr.eth()
        .send_raw_transaction(signed.raw_transaction)
        .await
        .c(d!())
}

// return: address + phrase
#[inline(always)]
pub fn create_account() -> (H160, Phrase) {
    let (keypair, phrase, _) = SecpPair::generate_with_phrase(None);
    let addr = keypair.address();
    (addr, phrase)
}

#[inline(always)]
pub fn recover_account_from_phrase(phrase: &str) -> Result<(H160, SecpPair)> {
    let keypair = SecpPair::from_phrase(phrase.trim(), None)
        .map(|(key_pair, _)| key_pair)
        .c(d!())?;
    let addr = keypair.address();
    Ok((addr, keypair))
}

#[inline(always)]
fn recover_prvk_from_phrase(phrase: &str) -> Result<Web3SecKey> {
    recover_account_from_phrase(phrase)
        .c(d!())
        .and_then(|(_, keypair)| recover_prvk_from_kp(&keypair).c(d!()))
}

#[inline(always)]
fn recover_prvk_from_kp(kp: &SecpPair) -> Result<Web3SecKey> {
    SecretKey::parse_slice(kp.seed().as_slice())
        .map(|prvk| to_hex(prvk.serialize()))
        .c(d!())
        .and_then(|prvk| Web3SecKey::from_str(&prvk).c(d!()))
}

// Convert an 'u128' to easy-readable format.
#[inline(always)]
fn to_float_str(n: u128) -> String {
    alt!(0 == n, return n.to_string());

    let base = 10u128.pow(18);
    let i = n / base;
    let j = n - i * base;

    let pads = if 0 == i {
        18 - (1..=18)
            .into_iter()
            .find(|&k| 0 == j / 10u128.pow(k))
            .unwrap()
    } else {
        0
    };
    let pads = (0..pads).map(|_| '0').collect::<String>();

    (i.to_string() + "." + &pads + j.to_string().trim_end_matches('0'))
        .trim_end_matches('.')
        .to_owned()
}

// Convert the amount in str format to an 'u128'.
#[inline(always)]
fn am_str_to_u128(am_str: &str) -> Result<u128> {
    am_str
        .parse::<f64>()
        .or_else(|_| am_str.parse::<u128>().map(|am| am as f64))
        .map(|am| (am * (10u128.pow(18) as f64)) as u128)
        .c(d!(format!("Invalid amount: {}", am_str)))
}

type TxHash = H256;
type Phrase = String;
// [Phrase, ReceiverRaw, AmountRaw]
type EntriesRaw = Vec<[String; 3]>;

struct Entry {
    phrase: Phrase,
    prvk: Web3SecKey,
    to: Address,
    am: u128,
}

struct Entries(Vec<Entry>);

impl TryFrom<EntriesRaw> for Entries {
    type Error = Box<dyn RucError>;
    fn try_from(er: EntriesRaw) -> Result<Self> {
        let mut ret = vec![];
        for [phrase, r, am] in er.into_iter() {
            let prvk = recover_prvk_from_phrase(&phrase).c(d!())?;
            let to = Address::from_str(&r).c(d!())?;
            let am = am_str_to_u128(&am).c(d!())?;
            ret.push(Entry {
                phrase,
                prvk,
                to,
                am,
            });
        }
        Ok(Entries(ret))
    }
}

impl From<Entries> for EntriesRaw {
    fn from(es: Entries) -> Self {
        let mut ret = vec![];
        for e in es.0.into_iter() {
            ret.push([e.phrase, format!("0x{:x}", e.to), e.am.to_string()]);
        }
        ret
    }
}
